﻿//-----------------------------------------------------------------------
// <copyright>
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//----------------------------------------------------------------------- 
using System;
using System.Threading;
using System.Windows.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Fulcrum.UnitTests
{
    delegate void OneParamAction<T>(T data);
    delegate TOut OneParamReturnAction<TIn,TOut>(TIn data);

    class UIThreadManager : IDisposable
    {
        private volatile Dispatcher dispatcher;
        private Thread uiThread;

        public UIThreadManager()
        {
            // nothing
        }

        public int UIThreadId
        {
            get
            {
                return this.uiThread.ManagedThreadId;
            }
        }

        public void Dispose()
        {
            StopUIThread();
        }

        public void StartUIThread()
        {
            if (this.dispatcher != null)
            {
                throw new InvalidOperationException("The TestWindow object has not been cleaned up yet. Use the Cleanup() method first.");
            }

            CreateUIThread();
        }

        private void CreateUIThread()
        {
            this.uiThread = new Thread(new ThreadStart(UIThreadStartingPoint));
            this.uiThread.SetApartmentState(ApartmentState.STA);
            this.uiThread.IsBackground = false;
            this.uiThread.Name = "UI Thread";
            this.uiThread.Start();

            // Wait till the windowDispetcher is set by the windowTread
            while (this.dispatcher == null)
            {
                Thread.Sleep(50);
            }
        }

        // This method is executed on the windowThread
        private void UIThreadStartingPoint()
        {
            this.dispatcher = Dispatcher.CurrentDispatcher;
            this.dispatcher.UnhandledException += new DispatcherUnhandledExceptionEventHandler(OnDispatcherUnhandledException);

            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.PushFrame(frame);
        }

        public void StopUIThread()
        {
            if (this.dispatcher != null)
            {
                //
                // Close the window and tell the UI thread to go away
                //

                this.dispatcher.Invoke(DispatcherPriority.Normal,
                      (NoParametersDelegate)delegate()
                      {
                          System.Windows.Threading.Dispatcher.CurrentDispatcher.InvokeShutdown();
                      });

                //
                // Wait for the UI thread to finish
                //
                this.uiThread.Join();
                this.uiThread = null;

                this.dispatcher.UnhandledException -= this.OnDispatcherUnhandledException;
                this.dispatcher = null;
            }
        }

        private delegate void NoParametersDelegate();

        private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            e.Handled = true;
            Assert.Fail("An unhandled exception occured on the UI thread: {0}", e.Exception);
        }

        /// <summary>
        /// Invoke a method on a control of type T in its own thread.
        /// </summary>
        /// <param name="callback">Test delegate method.</param>
        /// <exception cref="InvocationException">
        /// If the test fails, the exception will come back wrapped
        /// in <see cref="InvocationException"/>.
        /// </exception>
        public void Invoke<T>(OneParamAction<T> callback, T data)
        {
            VerifyUIThread();

            this.dispatcher.Invoke(callback, data);
        }

        /// <summary>
        /// Invoke a method on a control of type T in its own thread.
        /// </summary>
        /// <param name="callback">Test delegate method.</param>
        /// <exception cref="InvocationException">
        /// If the test fails, the exception will come back wrapped
        /// in <see cref="InvocationException"/>.
        /// </exception>
        public TOut Invoke<TIn, TOut>(OneParamReturnAction<TIn,TOut> callback, TIn data)
        {
            VerifyUIThread();

            return (TOut) this.dispatcher.Invoke(callback, data);
        }

        private void VerifyUIThread()
        {
            if (null == this.dispatcher)
            {
                throw new InvalidOperationException("Invoke may only be called when the UI thread is running.");
            }
        }

        public static void DoEvents(TimeSpan timeout)
        {
            var frame = new DispatcherFrame();
            var timer = new DispatcherTimer(DispatcherPriority.Normal);

            timer.Interval = timeout;
            timer.Tag = frame;
            timer.Tick += OnTimerTick;
            timer.Start();

            Dispatcher.PushFrame(frame);
        }

        public static void DoEvents(double timeoutMilliseconds)
        {
            DoEvents(TimeSpan.FromMilliseconds(timeoutMilliseconds));
        }

        static void OnTimerTick(object sender, EventArgs e)
        {
            var timer = (DispatcherTimer)sender;
            var frame = (DispatcherFrame)timer.Tag;

            timer.Stop();

            frame.Continue = false;
        }

        public static void DoEvents()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }

        private static object ExitFrame(object f)
        {
            var frame = (DispatcherFrame)f;
            frame.Continue = false;

            return null;
        }
    }
}
